package com.mini.cn.goods.io.netty.ws;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.Attribute;
import io.netty.util.CharsetUtil;
import org.apache.commons.lang3.StringUtils;

import java.util.HashSet;
import java.util.List;

import static io.netty.handler.codec.http.HttpHeaderNames.SEC_WEBSOCKET_KEY;
import static io.netty.handler.codec.http.HttpHeaderNames.SEC_WEBSOCKET_VERSION;
import static io.netty.handler.codec.http.HttpHeaderValues.UPGRADE;
import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpResponseStatus.*;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;

/**
 * Created with IntelliJ IDEA.

 * User: fly
 * Date: 2020-01-13
 * Time: 10:55 上午
 */
@ChannelHandler.Sharable
public final class HttpUpgradeHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    private final IWsHandel wsHandel;
    private final WebSocketServerHandler webSocketServerHandler;
    //指定的url
    private final HashSet<String> paths;
    //业务私有handle
    private final List<ChannelHandler> privateHandles;


    public HttpUpgradeHandler(IWsHandel handel, HashSet<String> paths, List<ChannelHandler> privateHandle) {
        this.wsHandel = handel;
        this.paths = paths;
        this.privateHandles = privateHandle;
        boolean isLastHandler = true;
        if (privateHandles != null && !privateHandles.isEmpty()) {
            isLastHandler = false;
        }
        webSocketServerHandler = new WebSocketServerHandler(wsHandel, isLastHandler);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
        try {
            handleHttpRequest(ctx, msg);
        } catch (Exception e) {
            FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, INTERNAL_SERVER_ERROR);
            sendHttpResponse(ctx, msg, res);
            e.printStackTrace();
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        if (wsHandel != null) {
            WebSocketSession session = ctx.channel().attr(WsConfig.SESSION_KEY).get();
            wsHandel.onError(session, cause);
        }
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        super.userEventTriggered(ctx, evt);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        if (wsHandel != null) {
            WebSocketSession session = ctx.channel().attr(WsConfig.SESSION_KEY).get();
            wsHandel.onClose(session);
        }
    }

    private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {
        FullHttpResponse res;
        // Handle a bad request.
        if (!req.decoderResult().isSuccess()) {
            res = new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST);
            sendHttpResponse(ctx, req, res);
            return;
        }
        // Allow only GET methods.
        if (req.method() != GET) {
            res = new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN);
            sendHttpResponse(ctx, req, res);
            return;
        }

        HttpHeaders headers = req.headers();
        String host = headers.get(HttpHeaderNames.HOST);
        if (StringUtils.isEmpty(host)) {
            res = new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN);
            sendHttpResponse(ctx, req, res);
            return;
        }

        String uri = req.uri();
        int index = uri.indexOf("?");
        String path = null;
        String originalParam = null;
        if (index == -1) {
            path = uri;
        } else {
            path = uri.substring(0, index);
            originalParam = uri.substring(index + 1);
        }
        if ("/favicon.ico".equals(path)) {
            res = new DefaultFullHttpResponse(HTTP_1_1, NOT_FOUND);
            sendHttpResponse(ctx, req, res);
            return;
        }
        //check path
        if (paths != null && !paths.isEmpty()) {
            if (!paths.contains(path)) {
                res = new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN);
                sendHttpResponse(ctx, req, res);
                return;
            }
        }
        //check header
        if (!req.headers().
                contains(UPGRADE) || !req.headers().
                contains(SEC_WEBSOCKET_KEY) || !req.headers().
                contains(SEC_WEBSOCKET_VERSION)) {
            res = new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN);
            sendHttpResponse(ctx, req, res);
            return;
        }
        // Handshake
        Channel channel = ctx.channel();
        WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(getWebSocketLocation(req),
                null, false, WsConfig.MAXFRAMEPAYLOADLENGTH);
        WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(req);
        if (handshaker == null) {
            WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(channel);
        } else {
            ChannelPipeline pipeline = ctx.pipeline();
            //握手成功以后 删除升级的handle
            pipeline.remove(ctx.name());
            //删除http协议的ChunkedWriteHandler
            if (pipeline.get(ChunkedWriteHandler.class) != null) {
                pipeline.remove(ChunkedWriteHandler.class);
            }
            //心跳30S readIdle 35S
            pipeline.addLast(new IdleStateHandler(35,
                    0, 0));
            //添加wsCompressHandle
            pipeline.addLast(new WebSocketServerCompressionHandler());
            pipeline.addLast(webSocketServerHandler);
            //添加私有的
            if (!webSocketServerHandler.isLastHandler) {
                pipeline.addLast(new LCSpliter());
                for (ChannelHandler handler : privateHandles) {
                    pipeline.addLast(handler);
                }
            }
            final String finalPath = path;
            final String finalOriginalParam = originalParam;
            handshaker.handshake(channel, req).addListener(future -> {
                WebSocketSession session = new WebSocketSession(channel);
                Attribute<WebSocketSession> attrSession = channel.attr(WsConfig.SESSION_KEY);
                attrSession.set(session);
                if (future.isSuccess()) {
                    if (wsHandel != null) {
                        final ParameterMap parameterMap = new ParameterMap(finalOriginalParam);
                        wsHandel.onOpen(session, headers, parameterMap);
                    }
                } else {
                    if (wsHandel != null) {
                        wsHandel.onHandShakeFail(session);
                    }
                    handshaker.close(channel, new CloseWebSocketFrame());
                }
            });
        }
    }

    private static void sendHttpResponse(
            ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) {
        // Generate an error page if response getStatus code is not OK (200).
        int statusCode = res.status().code();
        if (statusCode != OK.code() && res.content().readableBytes() == 0) {
            ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8);
            res.content().writeBytes(buf);
            buf.release();
        }
        HttpUtil.setContentLength(res, res.content().readableBytes());

        // Send the response and close the connection if necessary.
        ChannelFuture f = ctx.channel().writeAndFlush(res);
        if (!HttpUtil.isKeepAlive(req) || statusCode != 200) {
            f.addListener(ChannelFutureListener.CLOSE);
        }
    }

    private static String getWebSocketLocation(FullHttpRequest req) {
        String location = req.headers().get(HttpHeaderNames.HOST) + req.uri();
        return "ws://" + location;
    }
}
